﻿/*
 * Copyright(c) Live2D Inc. All rights reserved.
 * 
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at http://live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */


using Live2D.Cubism.Rendering;
using Live2D.Cubism.Rendering.Masking;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Live2D.Cubism.Editor.Importers;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor.Animations;
using System;

namespace Live2D.Cubism.Editor
{
	/// <summary>
	/// Hooks into Unity's asset pipeline allowing custom processing of assets.
	/// </summary>
	public class CubismAssetProcessor : AssetPostprocessor
	{
		#region Unity Event Handling

		/// <summary>
		/// Called by Unity. Makes sure <see langword="unsafe"/> code is allowed.
		/// </summary>
		// ReSharper disable once InconsistentNaming
		public static void OnGeneratedCSProjectFiles()
		{
			AllowUnsafeCode();
		}


		/// <summary>
		/// Called by Unity on asset import. Handles importing of Cubism related assets.
		/// </summary>
		/// <param name="importedAssetPaths">Paths of imported assets.</param>
		/// <param name="deletedAssetPaths">Paths of removed assets.</param>
		/// <param name="movedAssetPaths">Paths of moved assets</param>
		/// <param name="movedFromAssetPaths">Paths of moved assets before moving</param>
		private static void OnPostprocessAllAssets(
			string[] importedAssetPaths,
			string[] deletedAssetPaths,
			string[] movedAssetPaths,
			string[] movedFromAssetPaths)
		{
			//// Make sure builtin resources are available.
			//GenerateBuiltinResources();


			//// Handle any imported Cubism assets.
			//foreach (var assetPath in importedAssetPaths)
			//{
			//    var importer = CubismImporter.GetImporterAtPath(assetPath);


			//    if (importer == null)
			//    {
			//        continue;
			//    }

			//    importer.Import();
			//}
		}

		[MenuItem("IQIGame/IQIGame Tools/Live2D/强制所有资源重新生成")]
		public static void GenerateAssetsForce()
		{
			DoGenerateAssets(true);
		}

		[MenuItem("IQIGame/IQIGame Tools/Live2D/仅对新资源生成")]
		public static void GenerateAssets()
		{
			DoGenerateAssets(false);
		}

		public static void DoGenerateAssets(bool force)
		{
			try
			{
				GenerateBuiltinResources();

				string searchPath = Application.dataPath + "/20_Live2D/Resources";
				string[] dirs = Directory.GetDirectories(searchPath, "*", SearchOption.AllDirectories);

				DirectoryInfo dirInfoMoc = null;
				DirectoryInfo dirInfoMtn = null;
				EditorUtility.DisplayProgressBar("生成资源", "预设生成中......", 0);
				foreach (string dir in dirs)
				{
					DirectoryInfo dirInfo = new DirectoryInfo(dir);
					if (dirInfo.Name == "moc")
					{
						dirInfoMoc = dirInfo;
						GenerateModelPrefab(dirInfoMoc.FullName, force);
					}
				}

				EditorUtility.DisplayProgressBar("生成资源", "动画片段生成中......", 1 / 3f);
				foreach (string dir in dirs)
				{
					DirectoryInfo dirInfo = new DirectoryInfo(dir);
					if (dirInfo.Name == "mtn")
					{
						dirInfoMtn = dirInfo;
						GenerateMotions(dirInfoMtn.FullName, force);
					}
				}

				EditorUtility.DisplayProgressBar("生成资源", "动画控制器生成中......", 2 / 3f);
				foreach (string dir in dirs)
				{
					DirectoryInfo dirInfo = new DirectoryInfo(dir);
					if (dirInfo.Name == "mtn")
					{
						dirInfoMtn = dirInfo;
						GenerateAnimator(dirInfoMtn, force);
					}
				}
				EditorUtility.DisplayProgressBar("生成资源", "资源生成中......", 1f);

				AssetDatabase.SaveAssets();
				AssetDatabase.Refresh();

				EditorUtility.ClearProgressBar();
			}
			catch (Exception ex)
			{
				Debug.LogError(ex);
				EditorUtility.ClearProgressBar();
			}
		}

		private static void GenerateMotions(string srcDirPath, bool force)
		{
			if (Directory.Exists(srcDirPath) == false)
			{
				throw new System.Exception("路径不存在: " + srcDirPath);
			}

			string[] files = Directory.GetFiles(srcDirPath);

			foreach (string file in files)
			{
				if (file.EndsWith(".controller") && force == false)
				{
					return;
				}
			}

			foreach (string file in files)
			{
				if (file.EndsWith("motion3.json") == false)
				{
					continue;
				}
				string relativePath = file.Replace("\\", "/");
				relativePath = relativePath.Substring(relativePath.IndexOf("Assets/"));

				string data = AssetDatabase.LoadAssetAtPath<TextAsset>(relativePath).text;
				ICubismImporter importer = JsonUtility.FromJson(data, typeof(CubismMotion3JsonImporter)) as ICubismImporter;
				importer.SetAssetPath(relativePath);
				importer.Import();
			}
		}

		private static void GenerateModelPrefab(string srcDirPath, bool force)
		{
			if (Directory.Exists(srcDirPath) == false)
			{
				throw new System.Exception("路径不存在: " + srcDirPath);
			}

			string[] files = Directory.GetFiles(srcDirPath);

			foreach (string file in files)
			{
				if (file.EndsWith(".prefab") && force == false)
				{
					return;
				}
			}

			foreach (string file in files)
			{
				if (file.EndsWith("model3.json") == false)
				{
					continue;
				}
				string relativePath = file.Replace("\\", "/");
				relativePath = relativePath.Substring(relativePath.IndexOf("Assets/"));

				string data = AssetDatabase.LoadAssetAtPath<TextAsset>(relativePath).text;
				ICubismImporter importer = JsonUtility.FromJson(data, typeof(CubismModel3JsonImporter)) as ICubismImporter;
				importer.SetAssetPath(relativePath);
				importer.Import();
			}
		}

		private static void GenerateAnimator(DirectoryInfo dirMtn, bool force)
		{
			string mocPath = dirMtn.Parent.FullName + "/moc";
			DirectoryInfo dirMoc = new DirectoryInfo(mocPath);
			FileInfo filePrefab = dirMoc.GetFiles("*.prefab")[0];

			string controllerName = filePrefab.Name.Replace(filePrefab.Extension, ".controller");

			string controllerPath = dirMtn.FullName + "/" + controllerName;
			string controllerRelativePath = controllerPath.Replace("\\", "/");
			controllerRelativePath = controllerRelativePath.Substring(controllerRelativePath.IndexOf("Assets/"));
			var newConctroller = false;
			if (File.Exists(controllerPath) && force == false)
			{
				//return;
			}
			else
			{
				File.Delete(controllerPath);
				newConctroller = true;
			}

			string prefabRelativePath = filePrefab.FullName.Replace("\\", "/");
			prefabRelativePath = prefabRelativePath.Substring(prefabRelativePath.IndexOf("Assets/"));
			GameObject goPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabRelativePath);

			//Controller
			AnimatorController controller = null;
			if (newConctroller)
			{
				controller = AnimatorController.CreateAnimatorControllerAtPath(controllerRelativePath);

				//Add state
				AnimatorStateMachine sm = controller.layers[0].stateMachine;
				AnimatorState defaultState = sm.AddState("Default", new Vector3(270, 110, 0));
				sm.defaultState = defaultState;

				FileInfo[] fileInfos = dirMtn.GetFiles("*.anim");
				for (int i = 0; i < fileInfos.Length; i++)
				{
					FileInfo info = fileInfos[i];

					string relativePath = info.FullName.Replace("\\", "/");
					relativePath = relativePath.Substring(relativePath.IndexOf("Assets/"));
					AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(relativePath);

					AnimationClipSettings clipSetting = AnimationUtility.GetAnimationClipSettings(clip);
					if (relativePath.Contains("Stand") || relativePath.Contains("loop"))
					{
						clipSetting.loopTime = true;
					}
					else
					{
						clipSetting.loopTime = false;
					}
					AnimationUtility.SetAnimationClipSettings(clip, clipSetting);

					Vector3 pos = new Vector3(i % 4 * 200 - 270, i / 4 * 50 + 250, 0);
					AnimatorState state = sm.AddState(Path.GetFileNameWithoutExtension(relativePath), pos);
					state.motion = clip;
				} 		
			}
			else
			{
				controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerRelativePath);
			}
			goPrefab.GetComponent<Animator>().runtimeAnimatorController = controller;

            //CubismLookParameter 不能在游戏运行状态添加此组件，会失效
            GameObject goParameter = goPrefab.transform.Find("Parameters").gameObject;
            for (int i = 0; i < goParameter.transform.childCount; i++)
            {
                GameObject child = goParameter.transform.GetChild(i).gameObject;
                if (child.name.IndexOf("Look") != -1 && child.GetComponent<Live2D.Cubism.Framework.LookAt.CubismLookParameter>() == null)
                {
                    child.AddComponent<Live2D.Cubism.Framework.LookAt.CubismLookParameter>();
                }
            }

            AssetDatabase.SaveAssets();
			AssetDatabase.Refresh();
		}

		#endregion

		#region C# Project Patching

		/// <summary>
		/// Makes sure <see langword="unsafe"/> code is allowed in the runtime project.
		/// </summary>
		private static void AllowUnsafeCode()
		{
			foreach (var csproj in Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj"))
			{
				// Skip Editor assembly.
				if (csproj.EndsWith(".Editor.csproj"))
				{
					continue;
				}


				var document = XDocument.Load(csproj);
				var project = document.Root;


				// Allow unsafe code.
				for (var propertyGroup = project.FirstNode as XElement; propertyGroup != null; propertyGroup = propertyGroup.NextNode as XElement)
				{
					// Skip non-relevant groups.
					if (!propertyGroup.ToString().Contains("PropertyGroup") || !propertyGroup.ToString().Contains("$(Configuration)|$(Platform)"))
					{
						continue;
					}


					// Add unsafe-block element if necessary.
					if (!propertyGroup.ToString().Contains("AllowUnsafeBlocks"))
					{
						var nameSpace = propertyGroup.GetDefaultNamespace();


						propertyGroup.Add(new XElement(nameSpace + "AllowUnsafeBlocks", "true"));
					}


					// Make sure unsafe-block element is always set to true.
					for (var allowUnsafeBlocks = propertyGroup.FirstNode as XElement; allowUnsafeBlocks != null; allowUnsafeBlocks = allowUnsafeBlocks.NextNode as XElement)
					{
						if (!allowUnsafeBlocks.ToString().Contains("AllowUnsafeBlocks"))
						{
							continue;
						}


						allowUnsafeBlocks.SetValue("true");
					}
				}


				// Store changes.
				document.Save(csproj);
			}
		}

		#endregion

		#region Resources Generation

		/// <summary>
		/// Sets Cubism-style normal blending for a material.
		/// </summary>
		/// <param name="material">Material to set up.</param>
		private static void EnableNormalBlending(Material material,bool softMask = false)
		{
			var srcInt = (int)BlendMode.One;
			var dstInt = (int)BlendMode.OneMinusSrcAlpha;
			if (softMask)
			{
				srcInt = (int) BlendMode.SrcAlpha;
				//srcInt = (int) BlendMode.OneMinusSrcAlpha;
			}
			material.SetInt("_SrcColor", srcInt);
			material.SetInt("_DstColor", dstInt);
			material.SetInt("_SrcAlpha", srcInt);
			material.SetInt("_DstAlpha", dstInt);
		}

		/// <summary>
		/// Sets Cubism-style additive blending for a material.
		/// </summary>
		/// <param name="material">Material to set up.</param>
		private static void EnableAdditiveBlending(Material material)
		{
			material.SetInt("_SrcColor", (int)BlendMode.One);
			material.SetInt("_DstColor", (int)BlendMode.One);
			material.SetInt("_SrcAlpha", (int)BlendMode.Zero);
			material.SetInt("_DstAlpha", (int)BlendMode.One);
		}

		/// <summary>
		/// Sets Cubism-style multiplicative blending for a material.
		/// </summary>
		/// <param name="material">Material to set up.</param>
		private static void EnableMultiplicativeBlending(Material material)
		{
			material.SetInt("_SrcColor", (int)BlendMode.DstColor);
			material.SetInt("_DstColor", (int)BlendMode.OneMinusSrcAlpha);
			material.SetInt("_SrcAlpha", (int)BlendMode.Zero);
			material.SetInt("_DstAlpha", (int)BlendMode.One);
		}

		/// <summary>
		/// Enables Cubism-style masking for a material.
		/// </summary>
		/// <param name="material">Material to set up.</param>
		private static void EnableMasking(Material material)
		{
			// Set toggle.
			material.SetInt("cubism_MaskOn", 1);


			// Enable keyword.
			var shaderKeywords = material.shaderKeywords.ToList();


			shaderKeywords.RemoveAll(k => k == "CUBISM_MASK_OFF");


			if (!shaderKeywords.Contains("CUBISM_MASK_ON"))
			{
				shaderKeywords.Add("CUBISM_MASK_ON");
			}


			material.shaderKeywords = shaderKeywords.ToArray();
		}


		/// <summary>
		/// Generates the builtin resources as necessary.
		/// </summary>
		private static void GenerateBuiltinResources()
		{
			var resourcesRoot = AssetDatabase
				.GetAssetPath(CubismBuiltinShaders.Unlit)
				.Replace("/Shaders/Unlit.shader", "");


			// Create materials.
			if (CubismBuiltinMaterials.Mask == null)
			{
				var materialsRoot = resourcesRoot + "/Materials";


				// Make sure materials folder exists.
				if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), materialsRoot)))
				{
					Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), materialsRoot));
				}


				// Create mask material.
				var material = new Material(CubismBuiltinShaders.Mask)
				{
					name = "Mask"
				};


				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				// Create non-masked materials.
				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "Unlit"
				};

				EnableNormalBlending(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "UnlitAdditive"
				};

				EnableAdditiveBlending(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "UnlitMultiply"
				};

				EnableMultiplicativeBlending(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				// Create masked materials.
				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "UnlitMasked"
				};

				EnableNormalBlending(material);
				EnableMasking(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "UnlitAdditiveMasked"
				};

				EnableAdditiveBlending(material);
				EnableMasking(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				material = new Material(CubismBuiltinShaders.Unlit)
				{
					name = "UnlitMultiplyMasked"
				};

				EnableMultiplicativeBlending(material);
				EnableMasking(material);
				AssetDatabase.CreateAsset(material, string.Format("{0}/{1}.mat", materialsRoot, material.name));


				EditorUtility.SetDirty(CubismBuiltinShaders.Unlit);
				AssetDatabase.SaveAssets();
			}


			// Create global mask texture.
			if (CubismMaskTexture.GlobalMaskTexture == null)
			{
				var globalMaskTexture = ScriptableObject.CreateInstance<CubismMaskTexture>();

				globalMaskTexture.name = "GlobalMaskTexture";


				AssetDatabase.CreateAsset(globalMaskTexture, string.Format("{0}/{1}.asset", resourcesRoot, globalMaskTexture.name));
			}
		}

		#endregion
	}
}
